# Copyright (c) 2025, yuxinyong and contributors
# For license information, please see license.txt

import frappe
from frappe.model.document import Document


class PackingList(Document):
    def validate(self):
        self.sync_packing_details()

    def sync_packing_details(self):
        self.details = []
        for row in self.items:
            row_dict = row.as_dict()
            row_dict.pop('name')
            row_dict.pop('idx')
            details = pack_shoes(row_dict)
            for detail in details:
                row_dict.update(detail)
                self.append("details", row_dict)
                

def pack_shoes(style_data, allowed_carton_sizes=[], debug=False):
    """
    Packs shoes into cartons based on the given rules, prioritizing larger cartons
    and minimizing the total number of cartons by finding the best possible fit
    (single-size or mixed) for each carton.

    Args:
        style_data (dict): A dictionary containing shoe quantities per size for a style.
                           Example: {'STYLE': 'AGNES', 'qty_1': 4, ..., 'qty_15': 2, 'total_qty': 78}
        allowed_carton_sizes (list): A list of allowed carton capacities (e.g., [12, 9, 6, 4]).
        debug (bool): If True, print detailed debugging information during packing.

    Returns:
        list: A list of dictionaries, each representing a carton's packing details.
    """

    style_name = style_data.get('STYLE')
    if "allowed_carton_sizes" in style_data:
        allowed_carton_sizes = [int(q) for q in style_data["allowed_carton_sizes"].split(',')] 
    # Initialize all qty_x keys to 0, then update with provided data
    sizes_initial = {f'qty_{i}': 0 for i in range(1, 16)}
    for k, v in style_data.items():
        if k.startswith('qty_'):
            sizes_initial[k] = v

    # Convert to a dictionary with integer keys for sizes for easier manipulation
    available_shoes_dict = {int(k.split('_')[1]): v for k, v in sizes_initial.items()}
    
    packing_details = []
    carton_number = 1
    
    while sum(available_shoes_dict.values()) > 0: 
        if debug:
            print(f"\n--- Packing carton_id{carton_number} for {style_name} ---")
            print(f"Available shoes at start of round: {available_shoes_dict}")
        
        current_carton_contents = {f'qty_{i}': 0 for i in range(1, 16)}
        chosen_carton_capacity = 0 
        
        # This will hold the best pack found for the current carton (single or mixed).
        # Prioritize higher capacity (e.g., 9 over 6), then perfect utilization.
        best_pack_candidate = {
            'capacity': 0,       # The capacity of the carton chosen (e.g., 9 or 6)
            'actual_qty': 0,     # The actual quantity of shoes packed
            'contents': {},      # The dictionary of quantities for each size
            'type': 'none'       # For debugging: 'single' or 'mixed'
        }
        
        # Get sorted list of sizes with available shoes for consistent selection
        sorted_size_indices = sorted([s for s, q in available_shoes_dict.items() if q > 0])
        if debug:
            print(f"Sorted size indices available: {sorted_size_indices}")

        # Iterate through allowed carton capacities from largest to smallest to find the best fit
        for target_capacity in sorted(allowed_carton_sizes, reverse=True):
            if debug:
                print(f"  Attempting to find a pack for target capacity: {target_capacity}")

            # --- Evaluate Candidate 1: Perfect Single-Size Pack for this target_capacity ---
            # We prioritize a perfect single-size pack if it fills the largest possible carton.
            for size_idx in sorted_size_indices:
                if available_shoes_dict.get(size_idx, 0) >= target_capacity:
                    # Found a single size that can perfectly fill this target_capacity.
                    # If this is better than our current best candidate (i.e., higher capacity), update.
                    if target_capacity > best_pack_candidate['capacity']:
                        if debug:
                            print(f"    Found Single-Size perfect candidate for {target_capacity}: size {size_idx}")
                        best_pack_candidate = {
                            'capacity': target_capacity,
                            'actual_qty': target_capacity,
                            'contents': {f'qty_{size_idx}': target_capacity},
                            'type': 'single'
                        }
                        # No need to look for other single sizes for THIS target_capacity,
                        # but continue to check mixed for this capacity, and then smaller capacities.
                        break 
            
            # --- Evaluate Candidate 2: Perfect Mixed-Size Pack for this target_capacity ---
            temp_contents_mixed = {}
            temp_actual_qty_mixed = 0
            # Use a snapshot of available shoes for this trial to avoid modifying main dict prematurely
            temp_shoes_snapshot = available_shoes_dict.copy() 

            for size_idx in sorted_size_indices:
                qty_available = temp_shoes_snapshot.get(size_idx, 0)
                if qty_available > 0:
                    can_take = min(qty_available, target_capacity - temp_actual_qty_mixed)
                    if can_take > 0:
                        temp_contents_mixed[f'qty_{size_idx}'] = temp_contents_mixed.get(f'qty_{size_idx}', 0) + can_take
                        temp_actual_qty_mixed += can_take
                        temp_shoes_snapshot[size_idx] -= can_take
                        if temp_actual_qty_mixed == target_capacity:
                            break # Perfect fit for this target_capacity found
            
            # If a perfect mixed fit was found for the current target_capacity
            if temp_actual_qty_mixed == target_capacity:
                # Compare with current best_pack_candidate.
                # If this mixed pack fills a larger capacity, it's better.
                # If capacities are equal, a perfect mixed fit is as good as a perfect single-size fit for total cartons.
                if target_capacity > best_pack_candidate['capacity']:
                    if debug:
                        print(f"    Found Mixed-Size perfect candidate for {target_capacity}: {temp_contents_mixed}")
                    best_pack_candidate = {
                        'capacity': target_capacity,
                        'actual_qty': target_capacity,
                        'contents': temp_contents_mixed,
                        'type': 'mixed'
                    }

        # --- Apply the best perfect pack found, OR fallback to partial packing if no perfect fit ---
        if best_pack_candidate['actual_qty'] > 0: # A perfect fit (single or mixed) was found
            if debug:
                print(f"  Applying best perfect pack ({best_pack_candidate['type']}): {best_pack_candidate['contents']} into capacity {best_pack_candidate['capacity']}")
            chosen_carton_capacity = best_pack_candidate['capacity']
            for qty_key, qty_val in best_pack_candidate['contents'].items():
                size_idx = int(qty_key.split('_')[1])
                available_shoes_dict[size_idx] -= qty_val
                current_carton_contents[qty_key] = qty_val
        else:
            # No perfect full carton possible from any allowed capacity.
            # Now, attempt to pack as much as possible into the largest allowed carton size (even if not perfectly full).
            if debug:
                print("  No perfect full carton found. Attempting best partial fit.")
            max_actual_qty_packed = 0
            max_capacity_for_partial = 0
            best_partial_contents = {}

            # Iterate capacities from largest to smallest for partial fill
            for target_capacity in sorted(allowed_carton_sizes, reverse=True):
                temp_contents_partial = {}
                temp_actual_qty_partial = 0

                # Fill up the current target_capacity with available shoes (largest sizes first)
                for size_idx in sorted_size_indices:
                    qty_available = available_shoes_dict.get(size_idx, 0)
                    if qty_available > 0:
                        can_take = min(qty_available, target_capacity - temp_actual_qty_partial)
                        if can_take > 0:
                            temp_contents_partial[f'qty_{size_idx}'] = temp_contents_partial.get(f'qty_{size_idx}', 0) + can_take
                            temp_actual_qty_partial += can_take
                
                # If this partial pack is better (packs more, or same but into larger capacity)
                if temp_actual_qty_partial > max_actual_qty_packed:
                    max_actual_qty_packed = temp_actual_qty_partial
                    max_capacity_for_partial = target_capacity
                    best_partial_contents = temp_contents_partial
            
            if max_actual_qty_packed > 0:
                if debug:
                    print(f"  Applying best partial pack: {best_partial_contents} into capacity {max_capacity_for_partial}")
                chosen_carton_capacity = max_capacity_for_partial # The carton size used
                for qty_key, qty_val in best_partial_contents.items():
                    size_idx = int(qty_key.split('_')[1])
                    available_shoes_dict[size_idx] -= qty_val
                    current_carton_contents[qty_key] = qty_val
            else:
                if debug:
                    print("  No shoes left to pack in fallback. Breaking loop.")
                break # Should only happen if all available_shoes_dict.values() are 0
        
        # Add the packed carton details to the list
        row = {'carton_id': str(carton_number), 'STYLE': style_name}
        row.update(current_carton_contents)
        row['total_qty'] = chosen_carton_capacity
        row['number_of_cartons'] = 1
        
        packing_details.append(row)
        carton_number += 1
        
    # --- Merge consecutive identical cartons ---
    merged_packing_details = []
    if packing_details:
        current_merged_row = packing_details[0].copy()
        current_merged_row_start_num = int(current_merged_row['carton_id'])
        current_merged_row_count = 1
        
        for i in range(1, len(packing_details)):
            compare_row = packing_details[i]
            
            is_same = True
            # Check if contents and total_qty are identical for merging
            for key in current_merged_row:
                if key not in ['carton_id', 'number_of_cartons', 'STYLE']: # These keys are managed by merge logic
                    if current_merged_row[key] != compare_row[key]:
                        is_same = False
                        break
            
            if is_same: # If contents and total_qty are the same
                current_merged_row_count += 1
            else:
                # If current_merged_row_count is > 1, apply merge logic
                if current_merged_row_count > 1:
                    current_merged_row['carton_id'] = f"{current_merged_row_start_num}-{current_merged_row_start_num + current_merged_row_count - 1}"
                    current_merged_row['number_of_cartons'] = current_merged_row_count
                    current_merged_row['total_qty'] = current_merged_row['total_qty'] * current_merged_row_count
                merged_packing_details.append(current_merged_row)
                
                # Reset for the new sequence
                current_merged_row = compare_row.copy()
                current_merged_row_start_num = int(current_merged_row['carton_id'])
                current_merged_row_count = 1
        
        # Append the last merged block
        if current_merged_row_count > 1:
            current_merged_row['carton_id'] = f"{current_merged_row_start_num}-{current_merged_row_start_num + current_merged_row_count - 1}"
            current_merged_row['number_of_cartons'] = current_merged_row_count
            current_merged_row['total_qty'] = current_merged_row['total_qty'] * current_merged_row_count
        merged_packing_details.append(current_merged_row)
        
    return merged_packing_details

# Helper function to print results in a table format (replaces pandas.to_string)
def print_packing_list(packing_list, title="Packing Details", cols_order=None):
    if not packing_list:
        print(f"\n{title}:\nNo packing details to display.")
        return

    # Define standard column order if not provided
    if cols_order is None:
        cols_order = ['carton_id', 'STYLE'] + [f'qty_{i}' for i in range(1, 16)] + ['total_qty', 'number_of_cartons']

    # Calculate maximum column widths for alignment
    column_widths = {col: len(col) for col in cols_order}
    for row in packing_list:
        for col in cols_order:
            # Use .get(col, 0) to handle missing keys gracefully (treat as 0 quantity)
            val = row.get(col, 0) 
            column_widths[col] = max(column_widths[col], len(str(val)))

    # Print header
    header_line = "  ".join(f"{col:<{column_widths[col]}}" for col in cols_order)
    print(f"\n{title}:")
    print(header_line)
    print("-" * len(header_line)) # Separator line

    # Print rows
    for row in packing_list:
        row_values = []
        for col in cols_order:
            val = row.get(col, 0)
            row_values.append(f"{str(val):<{column_widths[col]}}")
        print("  ".join(row_values))

def test():
    # --- Sample Data Definitions ---
    sample_data_1 = {
        'carton_id': '', 'STYLE': 'AGNES', 'qty_1': 0, 'qty_2': 0, 'qty_3': 4, 'qty_4': 5, 'qty_5': 11, 'qty_6': 8,
        'qty_7': 16, 'qty_8': 10, 'qty_9': 10, 'qty_10': 5, 'qty_11': 6, 'qty_12': 1,
        'qty_13': 2, 'qty_14': 0, 'qty_15': 0, 'total_qty': 78, 'number_of_cartons': ''
    }
    allowed_sizes_1 = [9, 6]

    print_packing_list(
        pack_shoes(sample_data_1, allowed_sizes_1, debug=True), # Set debug=True to see detailed steps
        "Packing Details for Sample Data 1 (Attempt 10 - Combined Strategy No Pandas Debugging):"
    )
    print("\n" + "="*50 + "\n")

    sample_data_2 = {
        'carton_id': '', 'STYLE': 'CAMELLIA', 'qty_1': 0, 'qty_2': 0, 'qty_3': 13, 'qty_4': 15,
        'qty_5': 16, 'qty_6': 27, 'qty_7': 29, 'qty_8': 30, 'qty_9': 19, 'qty_10': 17,
        'qty_11': 14, 'qty_12': 2, 'qty_13': 2, 'qty_14': 0, 'qty_15': 0, 'total_qty': 184, 'number_of_cartons': ''
    }
    allowed_sizes_2 = [12, 6, 4]

    print_packing_list(
        pack_shoes(sample_data_2, allowed_sizes_2, debug=True), # Set debug=True to see detailed steps
        "Packing Details for Sample Data 2 (Attempt 10 - Combined Strategy No Pandas Debugging):"
    )